Chapter 8
Extending the User Interface

by Michael Morrison

In This Chapter

  Responding to the User 302
  User Interfaces and AppWizard 309
  Extending Menus 310
  Putting Control Bars to Use 314
  Updating the User Interface 324
  Property Sheets and Wizards 326
  Splitting a View 340

You’ve learned a fair amount about user interfaces up to this point in the book. However, you haven’t really looked into how to extend user interfaces and support modern features such as floating pop-up menus, control bars, and wizards. Users have come to expect flexible and intuitive user interfaces, and it’s your job to give them what they want. Fortunately, MFC includes plenty of support for building rich user interfaces without too much suffering on the part of the MFC programmer.

Indeed, supporting modern user interface features in your own applications is very straightforward thanks to MFC. This chapter shows you how to extend user interfaces and examines the MFC classes that make it possible to support these types of features.

Responding to the User

Before you get into the different ways to extend user interfaces with GUI elements, let’s take a moment to examine user input and how it is handled with MFC. This is important because you can greatly improve the feel of an application simply through the handling of the keyboard and mouse. Keyboard and mouse handling begins with a special set of messages that are sent when the user presses or releases a key on the keyboard, moves the mouse, or clicks a mouse button.

Handling keyboard and mouse messages is as simple as determining the specific messages you want to handle and then creating the appropriate message handlers for them. Win32 supports a variety of different keyboard and mouse messages, so it’s important to make sure you are handling the proper messages to achieve your desired functionality.

Keyboard Messaging

When you press a key in a Windows application, Windows generates a keyboard message. This message is sent to the application whose main frame window has keyboard focus. The application then handles the message and does whatever it needs to do with the keystroke information. In the case of a word processor, the application might store away a character based on the key press and draw the character on the screen.

Because internationalized applications must be capable of handling multiple languages, keyboard messages aren’t directly associated with specific characters on the keyboard. Instead, the Win32 API defines virtual key codes that are mapped to each key on the keyboard. Virtual key codes serve as device-independent identifiers for keys on the keyboard. Applications always interpret keystrokes as virtual key codes instead of raw characters. Following are some examples of virtual key codes defined in the Win32 API:

  VK_A
  VK_B
  VK_C
  VK_F1
  VK_F2
  VK_RETURN
  VK_DELETE

The routing of keyboard messages is based on which window has keyboard focus, which is a specific type of input focus. Input focus is a temporary property that only one window at a time is capable of having. Input focus is associated with the currently active window, which is often identifiable by a highlighted caption bar, dialog frame, or caption text, depending on the type of window. Although input focus plays an important role in determining where keyboard messages are sent, it doesn’t tell the whole story.

Keyboard focus determines when a window actually receives keyboard messages. Keyboard focus is a more specific type of input focus that requires that a window not be minimized. For example, a minimized word processor shouldn’t accept keyboard input because you can’t see what you’re typing. Minimized applications don’t receive keyboard messages because they don’t have keyboard focus.

Each time you press and release a key in Windows, a keyboard message is generated. In fact, an individual message is generated both for the key press and the key release. Table 8.1 lists the Win32 messages associated with keystrokes, along with the MFC message handlers for each.

Table 8.1 Win32 Keystroke Messages and Their MFC Message Handlers

Message Message Handler

WM_KEYDOWN OnKeyDown()
WM_KEYUP OnKeyUp()
WM_CHAR OnChar()
WM_SYSKEYDOWN OnSysKeyDown()
WM_SYSKEYUP OnSysKeyUp()

The WM_KEYDOWN and WM_KEYUP messages are used to process the vast majority of keystrokes. The WM_CHAR message is similar to the WM_KEYDOWN message, except it contains a translated character associated with the key press. The WM_CHAR message is sent after a WM_KEYDOWN and WM_KEYUP message combination. The WM_SYSKEYDOWN and WM_SYSKEYUP messages are sent in response to system keystrokes such as key combinations involving the Alt key. All the keyboard messages except WM_CHAR are sent in “key down”/”key up” pairs. However, when a key is held down and the typematic repeat for the keyboard kicks in, Windows sends a series of “key down” messages followed by a single “key up” message when the key is released.


Note:  

Windows automatically handles system keystrokes such as using the Alt key to access menus. It is rare that you would want to handle system keystrokes in an application. Windows also automatically handles keyboard accelerators, which are used as shortcuts to invoke some menu commands.


The OnKeyDown() message handler is relatively straightforward—it accepts a few parameters that contain information about the key press. Its declaration follows:

afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);

All the keyboard message handlers accept these same parameters. The nChar parameter specifies the virtual-key code of the key being pressed. The nRepCnt parameter specifies the repeat count, which applies if a key is being held down and the typematic repeat function of the keyboard is invoked. Finally, the nFlags parameter specifies additional information such as whether the Alt key was down when the key was pressed.

Handling Keyboard Messages

To handle keyboard messages, you must add a message map entry for each message handler you want to use. For example, the following message map entry is required to handle the WM_KEYDOWN message using the OnKeyDown() message handler:

ON_WM_KEYDOWN()

One interesting way to experiment with handling keyboard messages is to move the mouse cursor in response to the user pressing the arrow keys on the keyboard. The OnKeyDown() message handler is the only one you need to implement to carry out this functionality. Listing 8.1 contains the source code for an OnKeyDown() message handler that moves the mouse cursor in response to the user pressing the arrow keys.



Listing 8.1 An OnKeyDown() Message Handler that Moves the Mouse Cursor in Response to the Arrow Keys


void CMainFrame::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
 CPoint  ptCurPos;

 // Calculate new cursor position based on key press
 if (::GetCursorPos(&ptCurPos))
 {
    // Get the client area rect and convert to screen coordinates
   CRect rcClient;
   GetClientRect(&rcClient);
   ClientToScreen(&rcClient);

   switch (nChar) {
     case VK_LEFT:
       ptCurPos.x -= 5;
       if (rcClient.PtInRect(ptCurPos))
         ::SetCursorPos(ptCurPos.x, ptCurPos.y);
       break;

     case VK_RIGHT:
       ptCurPos.x += 5;
       if (rcClient.PtInRect(ptCurPos))
         ::SetCursorPos(ptCurPos.x, ptCurPos.y);
       break;

     case VK_UP:
       ptCurPos.y -= 5;
       if (rcClient.PtInRect(ptCurPos))
         ::SetCursorPos(ptCurPos.x, ptCurPos.y);
       break;

     case VK_DOWN:
       ptCurPos.y += 5;
       if (rcClient.PtInRect(ptCurPos))
         ::SetCursorPos(ptCurPos.x, ptCurPos.y);
       break;
   }
 }
}

The OnKeyDown() message handler gets the current mouse cursor position and modifies it based on the key pressed. The nChar parameter is used as the basis for determining which key was pressed. The four arrow keys are used to control the cursor, which explains the virtual key codes VK_LEFT, VK_RIGHT, VK_UP, and VK_DOWN. The cursor is actually moved by calling the Win32 API function SetCursorPos() and providing the new cursor position.

Mouse Messaging

Similar to keyboard messages, mouse messages are generated when you move the mouse or press a mouse button. Unlike keyboard messages, however, mouse messages are sent to any window that the mouse cursor passes over or that the mouse is clicked over, regardless of input focus. Every window is responsible for responding to mouse messages according to its own particular needs.


Note:  

Windows automatically handles many mouse functions, such as displaying menus that are clicked and altering the state of pushbuttons and check boxes that are clicked.


The mouse is represented on the screen by a mouse cursor, which typically takes the shape of an arrow. The mouse cursor has a single-pixel hotspot that pinpoints an exact location on the screen. The hotspot of the mouse cursor is significant because the position of all mouse operations is based on the hotspot. Figure 8.1 shows the hotspot location on the standard arrow mouse cursor.


Figure 8.1  The hotspot location on the standard arrow mouse cursor.

As an aspiring MFC guru, I’m sure you know the different ways a mouse can be used in Windows. However, it’s worth clarifying exactly what constitutes each different mouse operation because different mouse messages are generated based on how the mouse is used. Following are the different operations that can be performed with a mouse in Windows:

  Clicking—Pressing and releasing a mouse button
  Double-clicking—Pressing and releasing a mouse button twice in quick succession
  Moving—Moving the mouse around without pressing any buttons
  Dragging—Moving the mouse around while holding down a button

These operations determine the kinds of mouse messages generated by Windows. Mouse messages are divided into two types: client area messages and nonclient area messages. Client area messages are by far the more commonly used of the two types, and are therefore the ones you’re going to focus on. Table 8.2 lists the Win32 messages associated with the mouse, along with the MFC message handlers for each.


Note:  

The client area is the part of a window where an application displays output such as text in a word processor or graphics in a paint program. The nonclient area, on the other hand, includes the border, Maximize button, Minimize button, menu bar, scrollbar, title bar, and System menu.


Table 8.2 Win32 Mouse Messages and Their MFC Message Handlers

Message Message Handler

WM_MOUSEMOVE OnMouseMove()
WM_MOUSEACTIVATE OnMouseActivate()
WM_MOUSEHOVER OnMouseHover()
WM_MOUSELEAVE OnMouseLeave()
WM_MOUSEWHEEL OnMouseWheel()
WM_LBUTTONDOWN OnLButtonDown()
WM_MBUTTONDOWN OnMButtonDown()
WM_RBUTTONDOWN OnRButtonDown()
WM_LBUTTONUP OnLButtonUp()
WM_MBUTTONUP OnMButtonUp()
WM_RBUTTONUP OnRButtonUp()
WM_LBUTTONDBLCLK OnLButtonDblClk()
WM_MBUTTONDBLCLK OnMButtonDblClk()
WM_RBUTTONDBLCLK OnRButtonDblClk()

The WM_MOUSEMOVE message is sent when the mouse moves over the client area of a window. The WM_MOUSEACTIVATE message is sent when the mouse is clicked over a previously inactive window, thereby activating the window. The WM_MOUSEHOVER and WM_MOUSELEAVE messages are sent in response to the mouse being tracked through a call to TrackMouseEvent(). The WM_MOUSEHOVER message is sent if the mouse has not moved outside of a given rectangle in a specified period of time while being tracked; you might display a ToolTip in response to this message. The WM_MOUSELEAVE message is sent when the mouse leaves the client area of a window while being tracked.

The remaining mouse messages have to do with rotating the wheel of a mouse or clicking mouse buttons. The mouse wheel is a relatively new enhancement that appears on the Microsoft Intellimouse. Rotating the wheel of a mouse generates WM_MOUSEWHEEL messages, which contain information about how far the wheel was rotated. Most applications use the mouse wheel as an alternate means of scrolling. In other words, rotating the mouse wheel is equivalent to clicking the arrows on a scroll bar.

The WM_XBUTTONDOWN messages are sent when a mouse button is pressed within the client area of a window, whereas the WM_XBUTTONUP messages are sent when a mouse button is released. The WM_XBUTTONDBLCLK messages are sent when a mouse button is double-clicked in the client area of a window. All the button messages come in three versions that correspond to the left, right, and middle mouse buttons. Many mice don’t have a middle button, in which case they aren’t capable of generating middle mouse button messages.

Following is the declaration for the OnMouseMove() message handler, which indicates the parameters accepted by most of the mouse message handlers:

afx_msg void OnMouseMove(UINT nFlags, CPoint point);



The nFlags parameter indicates whether various virtual keys are down. This parameter can be any combination of the following values:

  MK_LBUTTON—Set if the left mouse button is down
  MK_MBUTTON—Set if the middle mouse button is down
  MK_RBUTTON—Set if the right mouse button is down
  MK_CONTROL—Set if the Ctrl key is down
  MK_SHIFT—Set if the Shift key is down

The other parameter, point, specifies the x and y coordinates of the mouse cursor. These coordinates indicate the specific position of the mouse cursor’s hotspot, and are relative to the upper-left corner of the window the mouse is over.

Handling Mouse Messages

It is common in many graphical drawing applications to use the mouse as a drawing tool; the user moves the mouse around and clicks to draw various shapes. As a simple example of how you might use the mouse in this type of application, consider the situation where clicking the left mouse button draws an image at the current mouse cursor position. This involves handling the WM_LBUTTONDOWN message, which requires implementing the OnLButton() message handler:

afx_msg void  OnLButtonDown(UINT nFlags, CPoint point);

Following is the message map entry required to use this message handler:

ON_WM_LBUTTONDOWN()

Listing 8.2 contains the source code for an OnLButtonDown() message handler that displays an image at the current mouse cursor position.is common in many graphical drawing applications to use the mouse

Listing 8.2 An OnLButtonDown() Message Handler that Draws an Image at the Current Mouse Cursor Position


void CMainFrame::OnLButtonDown(UINT nFlags, CPoint point) {
  CDC       dcMem;
  CBitmap   bm;
  CBitmap*  pbmOld;
  BITMAP    bmInfo;
  CSize     bmSize;

  // Load the bitmap
  VERIFY(bm.LoadBitmap(IDB_BITMAP));

  // Get the size of the bitmap
  bm.GetObject(sizeof(BITMAP), &bmInfo);
  bmSize.cx = bmInfo.bmWidth;
  bmSize.cy = bmInfo.bmHeight;

  // Setup the DCs
  CClientDC dc(this);
  dcMem.CreateCompatibleDC(&dc);
  VERIFY(pbmOld = dcMem.SelectObject(&bm));

  // Draw the image
  dc.BitBlt(point.x, point.y - bmInfo.bmHeight, bmSize.cx,
    bmSize.cy, &dcMem, 0, 0, SRCAND);

}

The OnLButtonDown() message handler first loads the bitmap using the resource identifier IDB_BITMAP, and then determines its size. The bitmap is then drawn to a device context at the position determined by the point parameter to the message handler. If you recall, this point specifies the position of the mouse cursor.is common in many graphical drawing applications to use the mouse

User Interfaces and AppWizard

Now that you have an understanding of how the keyboard and mouse are handled using MFC, you’re ready to move on to some more interesting user interface topics. Perhaps the best place to start is to assess the user interface features that are included in an application generated by AppWizard. As you know, AppWizard leads you through a series of questions that determine the type of application it generates. Following are application properties that directly correspond to user interface features prompted by AppWizard questions:

  SDI, MDI, or dialog-based application window
  Toolbar
  Status bar
  Printing and print preview
  3D controls
  Normal toolbars or Internet Explorer rebars

By simply checking the appropriate check boxes in AppWizard, you can gain these user interface features with very little work. Additionally, AppWizard provides a standard menu regardless of what options you select. This standard menu includes File, Edit, View, and Help pop-up menus, along with menu items in each that are associated with commonly used commands. For example, the View menu includes Toolbar and Status Bar menu items for showing and hiding an application’s toolbar and status bar. Also, the Edit menu includes menu items for the familiar Undo, Cut, Copy, and Paste commands.

If you’ve used Internet Explorer, you’re probably familiar with its toolbar, which is known as a rebar. A rebar is a control containing multiple bands that can each contain a toolbar or other control. Each band has a gripper bar that can be used to resize or reposition the band. A rebar ultimately functions in a manner similar to a traditional toolbar in that it contains buttons that act as shortcuts to menu commands. However, rebars are much more flexible and make it easier to create feature-rich toolbars.

You’ll learn how the menu, toolbar, and status bar AppWizard user interface features are implemented throughout the remainder of the chapter.

Extending Menus

You typically define an application menu using a menu resource that is placed in the application’s resource script. Although this approach certainly works well for defining the initial structure of a menu, in some situations you might want to alter a menu programmatically. You might even want to swap out a menu with an entirely different menu. MFC’s CMenu class gives you complete control over dynamically modifying menus. Following are the member functions that make this possible:

  GetSubMenu()—Gets a pop-up menu (submenu) within a menu
  GetMenuItemCount()—Gets the number of items in a menu
  AppendMenu()—Adds an item to the end of a menu
  InsertMenu()—Inserts an item at some point within a menu
  DeleteMenu()—Deletes a menu item
  ModifyMenu()—Modifies a menu item

These member functions enable you to dynamically alter a menu in just about any way you want. Before you move on to modifying a menu, however, let’s take a quick look at how menus and pop-up menus are retrieved.

Obtaining Menus and Pop-up Menus

Before you can modify a menu or pop-up menu, you must create a CMenu object that represents an application menu. Because most applications typically already have a menu, you can obtain a CMenu object pointer from an application frame window instead of creating a CMenu object yourself. You obtain a CMenu object pointer from an application frame window by calling the GetMenu() member function on the window object, like this:

CMenu* pMenu = GetMenu();

The menu pointer obtained with GetMenu() is associated with the top-level menu bar, and doesn’t enable direct access to pop-up menu items. Generally speaking, the majority of menu modification takes place in pop-up menus. So it is necessary to use a member function of CMenu to obtain a pop-up menu pointer. To get a menu pointer for a pop-up menu, you must call the GetSubMenu() function on the top-level menu object, like this:

CMenu* pFileMenu = GetMenu()->GetSubMenu(0);

This code gets a menu pointer to the first pop-up menu in the application menu, which is typically the File pop-up menu.

Adding Menu Items

After you obtain a CMenu object pointer, you are ready to use menu modification functions to dynamically alter the application menu. One of the most useful functions is InsertMenu(), which inserts a new menu item at a specific location within a menu. If you use the InsertMenu() function on a top-level menu, it will insert a new pop-up menu. To insert a menu item to the top of a pop-up menu, you must call the InsertMenu() member function on a submenu object:

pFileMenu->InsertMenu(0, MF_BYPOSITION | MF_STRING | MF_ENABLED,
 ID_FILE_CLOSE, “&Close”);



The first parameter is an integer that has different meanings based on the flags specified in the second parameter (see Table 8.3).

Table 8.3 Flags Used by the CMenu::InsertMenu() Member Function

Flag Description

MF_BYCOMMAND Indicates that the first parameter to InsertMenu() is the menu command identifier of an existing menu item; the new menu item is inserted at this position.
MF_BYPOSITION Indicates that the first parameter to InsertMenu() is a zero-based integer index for the menu position; the new menu item is inserted at this position.
MF_CHECKED The menu item is initially checked.
MF_UNCHECKED The menu item is initially unchecked.
MF_HILITE The menu item is initially highlighted.
MF_UNHILITE The menu item is initially unhighlighted.
MF_ENABLED The menu item is initially enabled.
MF_DISABLED The menu item is initially disabled.
MF_GRAYED The menu item is initially grayed.
MF_STRING The menu item is a normal menu item.
MF_POPUP The menu item is a new pop-up menu.

Based on the InsertMenu() flags shown in the table, you might be able to guess that the previous sample code adds a menu item named Close as the first item under the File pop-up menu. The integer identifier ID_FILE_CLOSE identifies the menu item and its associated command.

If you just want to add a menu item to the end of a pop-up menu, you can use the AppendMenu() member function, like this:

pHelpMenu->AppendMenu(MF_STRING | MF_ENABLED, ID_APP_ABOUT,
 “&About...”);

Using Floating Pop-up Menus

You’ve no doubt used floating pop-up menus in Windows applications such as Windows Explorer. These pop-up menus are typically context-sensitive, and are invoked with the right mouse button. For this reason, floating pop-up menus are sometimes referred to as context menus. Floating pop-up menus are very easy to implement in MFC applications thanks to a member function defined in the CMenu class.

The TrackPopupMenu() member function of CMenu is the key to using floating pop-up menus. TrackPopupMenu() displays a floating pop-up menu representing the CMenu object on which it is called. Following is an example of displaying a floating pop-up menu using the TrackPopupMenu() function:

pFileMenu->TrackPopupMenu(TPM_CENTERALIGN | TPM_LEFTBUTTON,
 x, y, this);

The first parameter to TrackPopupMenu() is a flag indicating the alignment and mouse button used to select menu items from the pop-up menu. Table 8.4 shows the flags used by TrackPopupMenu().

Table 8.4 Flags Used by the CMenu::TrackPopupMenu() Member Function

Flag Description

TPM_CENTERALIGN The menu is centered horizontally on the x position of the menu.
TPM_LEFTALIGN The left edge of the menu is aligned with the x position of the menu.
TPM_RIGHTALIGN The right edge of the menu is aligned with the x position of the menu.
TPM_LEFTBUTTON The left mouse button is used to make menu selections.
TPM_RIGHTBUTTON The right mouse button is used to make menu selections.

The x position referenced in the table is the second parameter to TrackPopupMenu(), which indicates the horizontal position of the menu relative to the screen. The third parameter is the y position of the menu. The last parameter is a pointer to the parent window of the menu; it is sufficient to pass this if you call TrackPopupMenu() from your main application window object.

Let’s take a look at how you might use TrackPopupMenu() in an application. Listing 8.3 contains an OnRButtonUp() message handler that displays a floating pop-up menu containing the menu commands for an application’s File menu.

Listing 8.3 An OnRButtonUp() Message Handler That Displays a Floating Pop-Up Menu


void CMainFrame::OnRButtonUp(UINT nFlags, CPoint point) {
 // Display the File menu as a floating pop-up menu
 CMenu* pFileMenu = GetMenu()->GetSubMenu(0);
 ClientToScreen(&point);
 pFileMenu->TrackPopupMenu(TPM_CENTERALIGN | TPM_LEFTBUTTON,
   point.x, point.y, this);
}

The OnRButtonUp() message handler first obtains a menu pointer for the File pop-up menu using the GetSubMenu() member function of CMenu. Before displaying the floating pop-up menu, OnRButtonUp() calls the ClientToScreen() member function of CWnd to convert the client window position in the point parameter to screen coordinates. This call is necessary because the TrackPopupMenu() function expects the pop-up menu position in screen coordinates, not client window coordinates. The OnRButtonUp() message handler finishes up by calling TrackPopupMenu() to display a pop-up menu containing the File menu commands.


Note:  

Although most floating pop-up menus are based on a submenu of a main application menu, you can also create custom pop-up menus as resources that aren’t necessarily used elsewhere in the application. To do this, you must first create a menu resource in the application’s resource file. You then create a CMenu object and call the LoadMenu() member function to load a menu from a resource. This menu object can then be used with the TrackPopupMenu() member function to display the menu as a floating pop-up menu.


Putting Control Bars to Use

Control bars are prevalent in most Windows applications and include toolbars, status bars, and dialog bars. The primary purpose of control bars is to enhance the user interface of applications and provide an alternative means of issuing commands and altering application settings. Toolbars and dialog bars are similar in that they both can contain buttons. However, dialog bars are more flexible in that they can contain any control. On the other hand, toolbars are easier to use and more efficient if you only need buttons.

Not surprisingly, MFC provides a set of classes that model each type of control bar, as well as a general base class from which each control bar class is derived. Following are the MFC classes that support control bars:

  CControlBar
  CToolBar
  CReBar
  CStatusBar
  CDialogBar



The CControlBar class provides the basic support required of all control bars, and serves as the base class for the other control bar classes. The CControlBar class supports a control bar window that can be aligned to the left or right edge of a frame window. Control bars can be docked, which allows a floating control bar to be attached to the edge of a frame window. Most control bars are docked by default; you initially make a control bar float by dragging it away from the edge of a frame window. Generally speaking, control bar windows are children of an application’s frame window, and siblings of the client window.

The CToolBar class implements a control bar with a row of bitmap buttons and optional separators. Each button in a toolbar contains a bitmap image, and can act like a pushbutton, a check box, or a radio button. In the latter two cases, a button remains depressed to indicate that it is checked. Toolbars are typically placed at the top of an application’s frame window in between the menu and the client area.

The CReBar class supports a rebar control that functions somewhat like an array of control bars. A rebar control contains multiple bands that can each contain a toolbar or other control. Each band in a rebar control has a gripper bar that can be used to resize or reposition the band. After creating a rebar control, you can add other control bars as bands of the rebar. For example, you could have three different toolbars that occupy different bands of a rebar control. You can also combine different types of control bars within a rebar, meaning that one band can be a toolbar while another is a dialog bar.

The CStatusBar class implements a control bar with a row of text panes that display information about the state of an application. These panes are commonly used to show help messages and keyboard status indicators such as the state of the CapsLock, NumLock, and ScrollLock keys. The most important usage of status bars is the display of help messages for menu commands. When the user highlights a command on a menu, a help message is displayed in the status bar containing information about the command. Status bars are typically placed at the bottom of an application’s frame window just below the client area.

The CDialogBar class implements a control bar that is much like a dialog box in that it can contain different types of controls. You typically create a dialog bar from a dialog box resource. Dialog bars are often placed at the top of an application’s frame window like a toolbar, but depending on the needs of a particular application, they might appear on the left or right side of the frame window, or even at the bottom.

Using Toolbars and Rebars

Toolbars consist of a row of bitmap buttons that are typically used as an alternative means of issuing menu commands. The majority of toolbar functionality is encapsulated in the CToolBar class, and is automatically enabled after you create and initialize a CToolBar object. Following are the steps required to create a toolbar:

1.  Create a toolbar bitmap and include it in the application as a bitmap resource.
2.  Create a toolbar resource.
3.  Create a CToolBar object.
4.  Call the Create() member function to create the toolbar.
5.  Call the LoadToolBar() member function to load the toolbar resource and bitmap.

Each button in a toolbar has a bitmap image associated with it that is displayed on the button. To make toolbar button images a little easier to manage, the images are specified in a single toolbar bitmap. You can think of this bitmap as an array of button images. Figure 8.2 shows a zoomed view of the default toolbar bitmap image generated by AppWizard.


Figure 8.2  A zoomed view of the default toolbar bitmap image generated by AppWizard.

As Figure 8.2 shows, individual button images are tiled from left to right across the toolbar bitmap. Each button image is 16×15 pixels, which is the standard size for most Windows applications. This image must be included as a bitmap resource in the resource script for the application to which you are adding the toolbar. After you’ve created a toolbar bitmap and included it as a bitmap resource, you need to create a toolbar resource. Listing 8.4 contains the toolbar resource generated by AppWizard for use with the toolbar bitmap you just saw.

Listing 8.4 The Default Toolbar Resource Definition Generated by AppWizard


IDR_MAINFRAME TOOLBAR DISCARDABLE  16, 15
BEGIN
  BUTTON      ID_FILE_NEW
  BUTTON      ID_FILE_OPEN
  BUTTON      ID_FILE_SAVE
  SEPARATOR
  BUTTON      ID_EDIT_CUT
  BUTTON      ID_EDIT_COPY
  BUTTON      ID_EDIT_PASTE
  SEPARATOR
  BUTTON      ID_FILE_PRINT
  SEPARATOR
  BUTTON      ID_APP_ABOUT
END

A toolbar resource is similar to a menu resource in that each button is defined by assigning it a menu command identifier. Keep in mind that the number of buttons in the toolbar resource must match the number of tiled button bitmaps in the toolbar bitmap. Also notice that the size of the button images (16×15) is specified at the start of the toolbar resource. Spaces between buttons are created using the SEPARATOR identifier.

After creating a toolbar resource, you need to create a CToolBar object. The first step in doing this is to declare a CToolBar member variable in the frame window of your application:

CToolBar m_wndToolBar;

The best place to create and initialize a toolbar is in the OnCreate() message handler for an application’s main frame window, which is associated with the WM_CREATE message. Listing 8.5 contains the code for an OnCreate() message handler that creates and initializes a toolbar.

Listing 8.5 An OnCreate() Message Handler that Creates a Toolbar


int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  // Create and load the toolbar
  if (!m_wndToolBar.Create(this) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
    return -1;

  return 0;
}

The Create() member function is first called on the CToolBar object to create the toolbar. However, at this stage the toolbar resource hasn’t been associated with the toolbar, so the toolbar really isn’t of much use. The toolbar is properly initialized by calling the LoadToolBar() member function, which loads the toolbar resource and bitmap.

Creating a toolbar as a band of a rebar control is also very straightforward. You must first declare a CReBar member variable in the frame window of your application:

CReBar m_wndReBar;



Then you create the CReBar object in the OnCreate() member function and call the AddBar() member function on it to add the toolbar. Listing 8.6 demonstrates how this is done.

Listing 8.6 An OnCreate() Message Handler That Creates a Toolbar as a Band of a Rebar


int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  // Create and load the toolbar
  if (!m_wndToolBar.Create(this) ||
  !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
    return -1;

  // Create the rebar and add the toolbar band
  if (!m_wndReBar.Create(this) || !m_wndReBar.AddBar(&m_wndToolBar))
    return -1;

  return 0;
}

Of course, you would typically want to create multiple control bars and add them to a rebar because the whole point of a rebar is to allow multiple bands.

You can modify an application’s toolbar using member functions defined in the CToolBar class. For example, the SetBitmap() member function allows you to change the tiled bitmap containing the individual button images. Also, the SetButtons() member function allows you to change the command identifiers associated with toolbar buttons. Following is an example of changing a toolbar to support only the commands on the File menu:

UINT[] cmds = { ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE };
m_wndToolBar.SetBitmap(hbmFile);
m_wndToolBar.SetButtons(cmds, 3);

This code assumes that you’ve already loaded a suitable toolbar bitmap image into the hbmFile bitmap handle. The SetButtons() member function accepts an array of command identifiers that are associated with the button images. The second parameter to SetButtons() indicates how many buttons are on the toolbar.

Although the user will typically take on the responsibility of docking a floating toolbar, you can also do so programmatically. The DockControlBar() member function of the CFrameWnd class performs exactly this task:

void DockControlBar(CControlBar* pBar, UINT nDockBarID = 0,
  LPCRECT lpRect = NULL);

Its first parameter is a pointer to the control bar you want to dock, and the second parameter is a constant identifying the side of the frame window to which the control is to dock. Table 8.5 lists the valid constants for the second parameter to DockControlBar().

Table 8.5 Constants That Identify the Side of the Frame Window for a Docked Control Bar

Constant Docks Control Bar to

AFX_IDW_DOCKBAR_TOP The top of the frame window
AFX_IDW_DOCKBAR_BOTTOM The bottom of the frame window
AFX_IDW_DOCKBAR_LEFT The left of the frame window
AFX_IDW_DOCKBAR_RIGHT The right of the frame window

The third parameter to DockControlBar() allows you to specify a rectangle in which the control bar is docked. This rectangle is specified in screen coordinates and indicates a location in the nonclient area of the frame window. You can accept the default value of NULL if you don’t want to specify a rectangle.

Using Status Bars

Status bars are used to convey information to the user such as help messages and keyboard status indicators. Help messages are displayed in the main section of a status bar, and other information is displayed in panes that appear inset on the status bar. In addition to keyboard status indicators, status bar panes can be used to display application-specific information.

Like toolbars, the functionality of status bars is largely automatic after you create and initialize a status bar object. Following are the steps required to create a status bar:

1.  Create string resources for the status bar help messages and keyboard status indicators.
2.  Create a CStatusBar object.
3.  Create a static UINT array specifying the panes for the status bar.
4.  Call the Create() member function to create the status bar.
5.  Call the SetIndicators() member function to set the panes for the status bar.

A status bar requires two different sets of string resources: a set identifying the help messages for menu commands and a set identifying the panes. Listing 8.7 contains string resources for status bar help messages.

Listing 8.7 Help Message String Resource Definitions for a Status Bar


STRINGTABLE
BEGIN
  ID_FILE_NEW        “Create a new document”
  ID_FILE_OPEN       “Open an existing document”
  ID_FILE_SAVE       “Save the document”
  ID_FILE_SAVE_AS    “Save the document with a new name”
  ID_FILE_PRINT      “Print the document”
  ID_EDIT_CUT        “Cuts the selection to the Clipboard”
  ID_EDIT_COPY       “Copies the selection to the Clipboard”
  ID_EDIT_PASTE      “Inserts Clipboard contents”
  ID_APP_ABOUT       “Displays program information”
  ID_APP_EXIT        “Quit the application; prompts to save document”
END

Associating status bar help messages with menu commands involves identifying the menu command identifier and the text message associated with it. Listing 8.8 is a little more interesting in that it specifies the keyboard status indicators used in the status bar panes.

Listing 8.8 Keyboard Status Indicator String Resource Definitions for a Status Bar


STRINGTABLE
BEGIN
  ID_INDICATOR_CAPS       “CAP”
  ID_INDICATOR_NUM        “NUM”
  ID_INDICATOR_SCRL       “SCRL”
END

The identifiers used in the keyboard status indicators are standard identifiers defined in MFC. They represent the CapsLock, NumLock, and ScrollLock keys, respectively. The purpose of these string resources is simply to determine the size of each keyboard status indicator pane within the status bar. More specifically, each pane is automatically sized so that it will fit the string resource. The creation of the panes is handled in a moment after you create the status bar object.

The creation of the CStatusBar object is handled by simply declaring a CMainFrame member variable of type CStatusBar:

CStatusBar m_wndStatusBar;

The panes for this status bar are specified using a static array of UINT pane identifiers:

static UINT indicators[] = { ID_SEPARATOR, ID_INDICATOR_CAPS,
  ID_INDICATOR_NUM, ID_INDICATOR_SCRL };

The ID_SEPARATOR identifier is used to place the panes on the right side of the status bar, which leaves room for help messages to be displayed on the left side. This array of identifiers is used to establish the status bar panes in the OnCreate() message handler, which is shown in Listing 8.9.



Listing 8.9 An OnCreate() Message Handler That Creates a Toolbar and a Status Bar


int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  // Create toolbar
  if (!m_wndToolBar.Create(this) ||
    !m_wndToolBar.LoadToolBar(IDR_TOOLBAR))
    return -1;

  // Create status bar
  if (!m_wndStatusBar.Create(this) ||
    !m_wndStatusBar.SetIndicators(indicators,
    sizeof(indicators)/sizeof(UINT)))
    return -1;

 return 0;
}

The status bar is created with a call to Create(), and the panes are set with a call to SetIndicators().

Showing and Hiding Control Bars

Control bars are typically considered an enhancement to an application’s graphical user interface, which means that they aren’t essential to using an application. Knowing this, there might be situations where a user would prefer not having them around. Therefore, it is nice to enable the user to hide control bars if desired. This gives users some flexibility in customizing an application to their own tastes, which is very important. Fortunately for you, it is easy to add this functionality to MFC applications.

The first step is to add menu commands that can be used to show and hide the toolbar and status bar of an application. Listing 8.10 shows a menu resource definition that includes a View pop-up menu containing the commands Toolbar and Status Bar.

Listing 8.10 A Menu Resource Definition That Includes a View Pop-up Menu


IDR_MENU
BEGIN
  POPUP “&File”
    BEGIN
      MENUITEM “&New\tCtrl+N”,          ID_FILE_NEW
      MENUITEM “&Open...\tCtrl+O”,      ID_FILE_OPEN
      MENUITEM “&Save\tCtrl+S”,         ID_FILE_SAVE
      MENUITEM “Save &As...”,           ID_FILE_SAVE_AS
      MENUITEM SEPARATOR
      MENUITEM “E&xit”,                 ID_APP_EXIT
    END
  POPUP “&Edit”
    BEGIN
      MENUITEM “&Undo\tCtrl+Z”,         ID_EDIT_UNDO
      MENUITEM SEPARATOR
      MENUITEM “Cu&t\tCtrl+X”,          ID_EDIT_CUT
      MENUITEM “&Copy\tCtrl+C”,         ID_EDIT_COPY
      MENUITEM “&Paste\tCtrl+V”,        ID_EDIT_PASTE
    END
  POPUP “&View”
    BEGIN
      MENUITEM “&Toolbar”,              ID_VIEW_TOOLBAR
      MENUITEM “&Status Bar”,           ID_VIEW_STATUS_BAR
    END
  POPUP “&Help”
    BEGIN
      MENUITEM “&About...”,             ID_APP_ABOUT
    END
END

The View menu commands use standard menu command identifiers (ID_VIEW_TOOLBAR and ID_VIEW_STATUS_BAR) that automatically invoke the show/hide functionality for control bars. In other words, an application gains the capability to show and hide its toolbar and status bar by simply supporting two menu commands.

Supporting ToolTips

The last control bar topic you’re going to learn about is ToolTips. If you recall, ToolTips are shorthand help messages that are displayed when you pause the mouse cursor over a toolbar button. Support for ToolTips is actually a toolbar style, which means that you can alter the style of a toolbar to enable ToolTips. You alter the style of a toolbar using the SetBarStyle() member function. The toolbar style for supporting ToolTips is CBRS_TOOLTIPS, which should be added to the toolbar styles when calling SetBarStyle(). Listing 8.11 contains an OnCreate() message handler that supports ToolTips.

Listing 8.11 An OnCreate() Message Handler That Supports ToolTips


int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
  if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
    return -1;

  // Create toolbar
  if (!m_wndToolBar.Create(this) ||
    !m_wndToolBar.LoadToolBar(IDR_TOOLBAR))
    return -1;

  // Create status bar
  if (!m_wndStatusBar.Create(this) ||
    !m_wndStatusBar.SetIndicators(indicators,
    sizeof(indicators)/sizeof(UINT)))
    return -1;

  // Enable tooltips
  m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() |
    CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);

  // Set tool bar as dockable
  m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
  EnableDocking(CBRS_ALIGN_ANY);
  DockControlBar(&m_wndToolBar);
  return 0;
}

This code creates a toolbar and status bar, and also sets up the toolbar to support ToolTips and docking. The SetBarStyle() member function is used to set the CBRS_TOOLTIPS style for the toolbar. Two other styles, CBRS_FLYBY and CBRS_SIZE_DYNAMIC, are also specified. These styles are used to jazz up the toolbar a little. The CBRS_FLYBY style results in the display of status bar help messages for toolbar buttons any time a ToolTip is activated. Without this style, a help message is only displayed when the user clicks a button. The CBRS_SIZE_DYNAMIC style allows the user to resize the toolbar window when it is detached from the application’s frame window and used as a floating toolbar.

At this point the toolbar is ready to use ToolTips, but you’ve yet to actually provide any of them. The actual text for ToolTips is provided as part of the status bar help messages for each menu command. You accomplish this by inserting a newline (\n) character at the end of each help message, followed by the ToolTip text. Listing 8.12 shows the new help message string resources with ToolTip text added.

Listing 8.12 A Help Message String Resource Definition for a Status Bar That Supports ToolTips


STRINGTABLE
BEGIN
  ID_FILE_NEW        “Create a new document\nNew”
  ID_FILE_OPEN       “Open an existing document\nOpen”
  ID_FILE_SAVE       “Save the document\nSave”
  ID_FILE_SAVE_AS    “Save the document with a new name\nSave As”
  ID_FILE_PRINT      “Print the document\nPrint”
  ID_EDIT_CUT        “Cuts the selection to the Clipboard\nCut”
  ID_EDIT_COPY       “Copies the selection to the Clipboard\nCopy”
  ID_EDIT_PASTE      “Inserts Clipboard contents\nPaste”
  ID_APP_ABOUT       “Displays program information\nAbout”
  ID_APP_EXIT        “Quit the application; prompts to save\nExit”
END

Updating the User Interface

Most commercial Windows applications intelligently enable and disable menu items based on the state of the application. Without MFC, injecting this type of menu state intelligence into an application is very tricky. MFC provides an elegant solution for managing the state of user interface elements including menu items, toolbar buttons, and status bar text. MFC’s solution to user interface state management comes in the form of command update message handlers. A command update message handler is associated with an individual user interface element such as a menu item or a toolbar button, and is called to refresh the state of the user interface element. For example, the command update message handler for a pop-up menu item is called just before the pop-up menu is displayed.

Each command update message handler requires a message map entry that ties the handler to a user interface element. The ON_UPDATE_COMMAND_UI macro is used to carry out this connection. Following is an example of wiring a command update message handler to a menu item:

ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, OnUpdateEditUndo)

This code wires the OnUpdateEditUndo() command update message handler to the menu item identified by ID_EDIT_UNDO. This handler is called just before the Edit menu item is displayed to the user. Following is the prototype of the OnUpdateEditUndo() command update message handler:

afx_msg void  OnUpdateEditUndo(CCmdUI* pCmdUI);



The parameter to OnUpdateEditUndo(), pCmdUI, is sent to all update command message handlers, and provides a means of modifying the state of the user interface element. Following are the primary member functions used in conjunction with the CCmdUI class:

  Enable()—Enables or disables the user interface element
  SetCheck()—Checks or unchecks the user interface element
  SetRadio()—Checks or unchecks the radio state of the user interface element
  SetText()—Sets the text of the user interface element

Following is an example of enabling a user interface element using the Enable() member function:

pCmdUI->Enable(TRUE);

You can disable a user interface element by calling the same member function and passing FALSE:

pCmdUI->Enable(FALSE);

The other member functions of the CCmdUI class work in roughly the same way. For example, the following code checks a user interface element:

pCmdUI->SetCheck(TRUE);

You might notice that menu items and toolbar buttons are disabled automatically if they don’t have command message handlers associated with them. This default behavior is carried out by MFC because a menu item or toolbar button is typically useless without a command message handler. When you implement a command message handler for a menu item or toolbar button, MFC automatically enables the item or button. You then control the state of the item or button using a command update message handler.

Property Sheets and Wizards

Property sheets have become an important part of the Windows user interface. A property sheet is a user interface component that presents multiple dialog boxes as pages within a single window; each page is accessed through a tab. The pages in a property sheet are called property pages and provide a solution to the problem of trying to gather lots of information within a single dialog box. Property pages enable you to use multiple dialog boxes within the same context, the property sheet. Wizards are very close relatives of property sheets and represent another user interface feature that has enjoyed widespread use in Windows.

Each property page in a property sheet is logically equivalent to an individual dialog box. In fact, you can think of the property sheet tabs as the names of the different dialog boxes (pages) contained within the property sheet. It isn’t just a coincidence that property pages appear to be so similar to dialog boxes; property pages are actually implemented as dialog boxes. In fact, you define the layout of individual property pages using dialog box resources.

Property sheets are different from wizards in that property sheets enable the user to click a tab and go to any page at will. Wizards are more restrictive and enforce an ordered approach to navigating the pages. To enforce this page order, wizards don’t provide tabs for page navigation. Also, property sheets don’t provide Back or Next buttons to navigate between pages because the user can just use the tabs.

Other than navigational differences, property sheets and wizards are actually very much alike. Following are some of the similarities between them:

  They serve as a container for multiple pages.
  Their pages are laid out as dialog box resources.
  They provide a means of navigating through the pages.
  They automatically provide buttons for performing standard tasks such as OK, Finish, Cancel, and so on.

Although wizards and property sheets differ slightly in their approaches to some of these things, such as navigating through pages, they are still very similar. In fact, creating a property page is little different than creating a wizard; the same MFC class is used as the basis for creating both property pages and wizards.

MFC’s Support for Property Sheets and Wizards

There are two MFC classes that encapsulate the functionality of both property pages and wizards:

  CPropertySheet—Represents both property sheets and wizards
  CPropertyPage—Represents pages within a property sheet or wizard

The CPropertySheet Class

Property sheets and wizards are implemented in the CPropertySheet class, which is derived from CWnd. The CPropertySheet class serves as a container for property pages, which themselves represent dialog boxes. Within an application, a property sheet consists of a CPropertySheet object and multiple CPropertyPage objects. You can communicate with the individual pages in a property sheet by calling member functions of the CPropertySheet class.

Although the CPropertySheet class is derived from CWnd, interacting with a property sheet or wizard is a lot like interacting with a dialog box through a CDialog object. To invoke a property sheet, you use the familiar two-step process required of dialog boxes:

1.  Create a CPropertySheet object.
2.  Call the DoModal() member function to display the property sheet or wizard as a modal dialog box.


Note:  

The CPropertySheet class also supports modeless property sheets, in which case you use the Create() member function instead of DoModal(). Modeless property sheets are generally more complex to implement than their modal counterparts. Technically speaking, you could also create a modeless wizard, but it probably wouldn’t make much sense.


Following is an example of creating a property sheet that is implemented in a CPropertySheet-derived class named CMyPropSheet:

CMyPropSheet propSheet;
propSheet.DoModal();

Creating and displaying a wizard requires an additional member function call to indicate that the property sheet is in fact a wizard:

CMyWizard wiz;
wiz.SetWizardMode();
wiz.DoModal();

The SetWizardMode() member function establishes that a property sheet is to operate as a wizard. The result is that Back and Next buttons are used to navigate the pages instead of tabs. The Finish button is also typically used on the last page to indicate that the user is finished with the wizard. When you call the DoModal() member function on a wizard, it will return ID_WIZFINISH or IDCANCEL, depending on whether the user clicked the Finish button or the Cancel button to exit the wizard.

Getting back to the CPropertySheet class, Table 8.6 lists some of the member functions commonly used with wizards.

Table 8.6 The Most Commonly Used CPropertySheet Member Functions

Member Function Description

DoModal() Displays the wizard as a modal dialog box
SetWizardMode() Forces the property sheet to operate as a wizard
SetWizardButtons() Determines which wizard buttons are displayed/enabled
AddPage() Adds a page to the wizard
RemovePage() Removes a page from the wizard



The SetWizardButtons() member function is called to specify which buttons are displayed in the wizard. You will typically call this function within a property page to set the wizard buttons when the page becomes active. SetWizardButtons() accepts a flag that can be a combination of the following button identifiers:

  PSWIZB_BACK—Back button
  PSWIZB_NEXT—Next button
  PSWIZB_FINISH—Finish button
  PSWIZB_DISABLEDFINISH—Disabled Finish button

The AddPage() and RemovePage() member functions of CProperySheet are used to add and remove pages in a wizard. Following is an example of how the AddPage() function is used:

CWizPage1 page1;
page1.Construct(IDD_WIZPAGE1, 0);
AddPage(&page1);

This creates a property page and adds it to a property sheet. The property page is based on the dialog box resource identified by IDD_WIZPAGE1. This identifier is passed into the Construct() member function of the property page.

The CPropertyPage Class

The CPropertyPage class provides the functionality required of a dialog-based property page. Table 8.7 lists the most commonly used member functions defined in the CPropertyPage class.

Table 8.7 The Most Commonly Used CPropertyPage Member Functions

Member Function Description

Contruct() Constructs the property page
OnSetActive() Called when the page becomes active
OnKillActive() Called when the page is no longer active
OnWizardBack() Called when the user clicks the Back button
OnWizardNext() Called when the user clicks the Next button
OnWizardFinish() Called when the user clicks the Finish button

In the previous section, you saw how to use the Construct() member function. The remaining member functions in Table 8.7 are called in response to different events. The last three member functions apply only to wizards.

Because the CPropertyPage class is derived from CDialog, you use it much as you would use the CDialog class. A property page class derived from CPropertyPage can override the OnInitDialog() member function to perform any one-time initialization chores. Property page classes also have message maps that enable them to respond to messages just like dialog boxes.

Creating a Simple Wizard

Let’s pull together what you’ve learned about property sheets and wizards to create a functioning example. The next few sections lead you through the development of a wizard named Investment that calculates interest on an investment. You could feasibly use this wizard as part of a financial application. The Investment Wizard consists of three pages that gather information about an investment and display the results. Figures 8.3 through 8.5 show the finished Investment Wizard as it appears in an application. This should give you a good idea about what you are creating as you work through the code for the wizard.


Figure 8.3  The first page of the Investment Wizard.


Figure 8.4  The second page of the Investment Wizard.


Figure 8.5  The third page of the Investment Wizard.

The best place to start when building a wizard is to assemble the dialog box resources for each wizard page. Because the Investment Wizard is ultimately part of an application, its resources must be defined along with the application’s resources. Listing 8.13 contains resource identifiers for the Investment Wizard’s dialog boxes, controls, and bitmaps.

Listing 8.13 Resource Identifiers for the Investment Wizard


// Dialog Boxes
#define IDD_INVESTWIZPAGE1    1000
#define IDD_INVESTWIZPAGE2    1001
#define IDD_INVESTWIZPAGE3    1002

// Controls
#define IDC_FNAME             2000
#define IDC_MINITIAL          2001
#define IDC_LNAME             2002
#define IDC_AMOUNT            2003
#define IDC_RETURNRATE        2004
#define IDC_PERIOD            2005
#define IDC_RESULT            2006

// Bitmaps
#define IDB_INVESTWIZPAGE1    3000
#define IDB_INVESTWIZPAGE2    3001
#define IDB_INVESTWIZPAGE3    3002

The dialog box resources for each page in the Investment Wizard are identified by the constants IDD_INVESTWIZPAGE1, IDD_INVESTWIZPAGE2, and IDD_INVESTWIZPAGE3. The control identifiers are associated with controls in each of the dialog boxes. As you might have noticed in Figures 8.3-8.5, which show the completed Investment Wizard, each page of the wizard displays a bitmap. These bitmaps are identified by the constants IDB_INVESTWIZPAGE1, IDB_INVESTWIZPAGE2, and IDB_INVESTWIZPAGE3. The resources required of the wizard, including the bitmaps and dialog boxes, are shown in Listing 8.14. These resources should be included in the resource script for the application that uses the wizard.

Listing 8.14 Resources for the Investment Wizard


// Bitmaps
IDB_INVESTWIZPAGE1 BITMAP “IWPage1.bmp”
IDB_INVESTWIZPAGE2 BITMAP “IWPage2.bmp”
IDB_INVESTWIZPAGE3 BITMAP “IWPage3.bmp”

// Dialog Boxes
IDD_INVESTWIZPAGE1 DIALOGEX 0, 0, WIZ_CXDLG, WIZ_CYDLG
CAPTION “Investment Wizard -- First Page”
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
FONT 8, “MS Sans Serif”
BEGIN
  RTEXT         “First Name:”, IDC_STATIC, 106, 10, 42, 8, 0
  EDITTEXT      IDC_FNAME, 152, 7, 99, 12
  RTEXT         “Middle Initial:”, IDC_STATIC, 106, 25, 42, 8, 0
  EDITTEXT      IDC_MINITIAL, 152, 22, 16, 12
  RTEXT         “Last Name:”, IDC_STATIC, 106, 42, 42, 8, 0
  EDITTEXT      IDC_LNAME, 152, 38, 99, 12
END

IDD_INVESTWIZPAGE2 DIALOGEX 0, 0, WIZ_CXDLG, WIZ_CYDLG
CAPTION “Investment Wizard -- Second Page”
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
FONT 8, “MS Sans Serif”
BEGIN
  RTEXT         “Investment Amount:”, IDC_STATIC, 112, 10, 64, 8, 0
  RTEXT         “Rate of Return:”, IDC_STATIC, 112, 26, 64, 8, 0
  EDITTEXT      IDC_AMOUNT, 180, 8, 66, 12, ES_NUMBER
  EDITTEXT      IDC_RETURNRATE, 180, 24, 23, 12, ES_NUMBER
  LTEXT         “%”, IDC_STATIC, 206, 27, 8, 8
  RTEXT         “Investment Period:”, IDC_STATIC, 112, 42, 64, 8, 0
  EDITTEXT      IDC_PERIOD, 180, 40, 23, 12, ES_NUMBER
  LTEXT         “years”, IDC_STATIC, 207, 43, 17, 8
END

IDD_INVESTWIZPAGE3 DIALOGEX 0, 0, WIZ_CXDLG, WIZ_CYDLG
CAPTION “Investment Wizard -- Third Page”
STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
FONT 8, “MS Sans Serif”
BEGIN
  LTEXT         “Results:”, IDC_STATIC, 112, 10, 30, 8, 0
  LTEXT         “”, IDC_RESULT, 146, 8, 120, 60, 0
END

Perhaps the most important thing to notice about the dialog box resources for the Investment Wizard is their width and height. The dialog boxes are set to the standard wizard page dimensions defined by the WIZ_CXDLG and WIZ_CYDLG constants. These constants are defined in the standard PrSht.h header file, which must be included in the resource script containing the wizard resources.

Although you can’t tell it by looking at the resource code, the controls in the wizard page dialog boxes are intentionally placed to the right of the boxes to make room for the bitmaps. The bitmaps are drawn in the source code for the property pages, which you’ll learn about in a moment.



Wizard Data

Before moving on to the page classes and wizard class for the Investment Wizard, it is helpful to define a couple of global variables used by them:

CInvestmentWiz* pInvestmentWiz;
CInvestWizData  wizData;

These global variables are defined in the InvestmentWiz.cpp source code file along with the page classes and wizard class. The pInvestmentWiz pointer keeps track of the wizard object for the sake of the page objects. This is necessary so that the pages can call SetWizardButtons() on the wizard to display and enable the appropriate wizard buttons.

The wizData global variable is of type CInvestWizData, which is a custom data class used to hold the information edited by the user in the wizard. Listing 8.15 contains the declaration of the CInvestWizData class.

Listing 8.15 The CInvestWizData Class Declaration


class CInvestWizData : public CObject {
  // Public Member Data
public:
  CString m_sFName;
  CString m_sMInitial;
  CString m_sLName;
  float   m_fAmount;
  float   m_fReturnRate;
  int     m_nPeriod;

  // Public Constructor(s)/Destructor
public:
  CInvestWizData();
};

The CInvestWizData class provides member variables that keep track of the state of the wizard. Sometimes you will want to include member data directly in the property pages, but in this case you need to share the information across the entire wizard. So it makes sense to place the data in a global object. Listing 8.16 contains the code for the CInvestWizData constructor, which provides initial values for the member data.

Listing 8.16 The CInvestWizData:: CInvestWizData() Constructor


// CInvestWizData Public Constructor(s)/Destructor
CInvestWizData::CInvestWizData() {
  // Initialize the wizard data
  m_sFName = “John”;
  m_sMInitial = “D”;
  m_sLName = “Doe”;
  m_fAmount = 1000.0;
  m_fReturnRate = 8.0;
  m_nPeriod = 10;
}

Wizard Pages

The Investment Wizard page classes closely resemble dialog box classes. Listing 8.17 contains the class declaration for the CInvestWizPage1 wizard page class.

Listing 8.17 The CInvestWizPage1 Class Declaration


class CInvestWizPage1 : public CPropertyPage {
  // Private Member Data
private:
  CBitmap m_bitmap;

  // Public Member Functions
public:
  BOOL          OnInitDialog();
  BOOL          OnSetActive();
  void          DoDataExchange(CDataExchange* pDX);

  // Message Handlers
public:
  afx_msg void  OnPaint();

  // Message Map & Runtime Support
protected:
  DECLARE_MESSAGE_MAP()
};

The OnPaint() message handler is used to paint the bitmap on the page. Painting on a dialog box isn’t all that common, but in the case of wizards it makes perfect sense. Listing 8.18 contains the implementation of the CInvestWizPage1 wizard page class.

Listing 8.18 The CInvestWizPage1 Class Implementation


// CInvestWizPage1 Message Map & Runtime Support
BEGIN_MESSAGE_MAP(CInvestWizPage1, CPropertyPage)
  ON_WM_PAINT()
END_MESSAGE_MAP()

// CInvestWizPage1 Public Member Functions
BOOL CInvestWizPage1::OnInitDialog() {
  // Load the bitmap
  m_bitmap.LoadBitmap(IDB_INVESTWIZPAGE1);

  return TRUE;
}

BOOL CInvestWizPage1::OnSetActive() {
  // Show only the Next button
  pInvestmentWiz->SetWizardButtons(PSWIZB_NEXT);

  // Set the control values
  SetDlgItemText(IDC_FNAME, wizData.m_sFName);
  SetDlgItemText(IDC_MINITIAL, wizData.m_sMInitial);
  SetDlgItemText(IDC_LNAME, wizData.m_sLName);

  return TRUE;
}

void CInvestWizPage1::DoDataExchange(CDataExchange* pDX) {
  CDialog::DoDataExchange(pDX);

  // Exchange the dialog data
  DDX_Text(pDX, IDC_FNAME, wizData.m_sFName);
  DDX_Text(pDX, IDC_MINITIAL, wizData.m_sMInitial);
  DDX_Text(pDX, IDC_LNAME, wizData.m_sLName);
}

// CInvestWizPage1 Message Handlers
void CInvestWizPage1::OnPaint() {
  CPaintDC dc(this);

  // Select the bitmap into a memory DC
  CDC dcMem;
  dcMem.CreateCompatibleDC(&dc);
  dcMem.SelectObject(&m_bitmap);

  // Copy the bitmap to the dialog DC
  dc.BitBlt(0, 0, 129, 226, &dcMem, 0, 0, SRCCOPY);
}

The bitmap for the page is loaded in the OnInitDialog() member function. The OnSetActive() member function is then used to set the wizard buttons for the page, which in this case consist only of the Next button. OnSetActive() also takes on the task of initializing the dialog controls with the global wizard data that applies to this page. This global data is updated in the DoDataExchange() member function, which wires variables to dialog controls. Finally, the OnPaint() message handler handles drawing the page’s bitmap on the left side of the page.

The remaining two wizard page classes, CInvestWizPage2 and CInvestWizPage3, are implemented very similarly to CInvestWizPage1. Listings 8.19 through 8.22 contain the code for these classes.

Listing 8.19 The CInvestWizPage2 Class Declaration


class CInvestWizPage2 : public CPropertyPage {
  // Private Member Data
private:
  CBitmap m_bitmap;

  // Public Member Functions
public:
  BOOL          OnInitDialog();
  BOOL          OnSetActive();
  void          DoDataExchange(CDataExchange* pDX);

  // Message Handlers
public:
  afx_msg void  OnPaint();

  // Message Map & Runtime Support
protected:
  DECLARE_MESSAGE_MAP()
};

Listing 8.20 The CInvestWizPage2 Class Implementation


// CInvestWizPage2 Message Map & Runtime Support
BEGIN_MESSAGE_MAP(CInvestWizPage2, CPropertyPage)
  ON_WM_PAINT()
END_MESSAGE_MAP()

// CInvestWizPage2 Public Member Functions
BOOL CInvestWizPage2::OnInitDialog() {
  // Load the bitmap
  m_bitmap.LoadBitmap(IDB_INVESTWIZPAGE2);

  return TRUE;
}

BOOL CInvestWizPage2::OnSetActive() {
  // Show the Back and Next buttons
  pInvestmentWiz->SetWizardButtons(PSWIZB_BACK | PSWIZB_NEXT);

  // Set the control values
  CString sVal;
  sVal.Format(“%.2f”, wizData.m_fAmount);
  SetDlgItemText(IDC_AMOUNT, sVal);
  sVal.Format(“%.2f”, wizData.m_fReturnRate);
  SetDlgItemText(IDC_RETURNRATE, sVal);
  sVal.Format(“%d”, wizData.m_nPeriod);
  SetDlgItemText(IDC_PERIOD, sVal);

  return TRUE;
}

void CInvestWizPage2::DoDataExchange(CDataExchange* pDX) {
  CDialog::DoDataExchange(pDX);

  // Exchange the dialog data
  DDX_Text(pDX, IDC_AMOUNT, wizData.m_fAmount);
  DDX_Text(pDX, IDC_RETURNRATE, wizData.m_fReturnRate);
  DDX_Text(pDX, IDC_PERIOD, wizData.m_nPeriod);
}

// CInvestWizPage2 Message Handlers
void CInvestWizPage2::OnPaint() {
  CPaintDC dc(this);

  // Select the bitmap into a memory DC
  CDC dcMem;
  dcMem.CreateCompatibleDC(&dc);
  dcMem.SelectObject(&m_bitmap);

  // Copy the bitmap to the dialog DC
  dc.BitBlt(0, 0, 129, 226, &dcMem, 0, 0, SRCCOPY);
}



Listing 8.21 The CInvestWizPage3 Class Declaration


class CInvestWizPage3 : public CPropertyPage {
  // Private Member Data
private:
  CBitmap m_bitmap;

  // Public Member Functions
public:
  BOOL          OnInitDialog();
  BOOL          OnSetActive();

  // Message Handlers
public:
  afx_msg void  OnPaint();

  // Message Map & Runtime Support
protected:
 DECLARE_MESSAGE_MAP()
};

Listing 8.22 The CInvestWizPage3 Class Implementation


// CInvestWizPage3 Message Map & Runtime Support
BEGIN_MESSAGE_MAP(CInvestWizPage3, CPropertyPage)
  ON_WM_PAINT()
END_MESSAGE_MAP()

// CInvestWizPage3 Public Member Functions
BOOL CInvestWizPage3::OnInitDialog() {
  // Load the bitmap
  m_bitmap.LoadBitmap(IDB_INVESTWIZPAGE3);

  return TRUE;
}

BOOL CInvestWizPage3::OnSetActive() {
  // Show the Back and Finish buttons
  pInvestmentWiz->SetWizardButtons(PSWIZB_BACK | PSWIZB_FINISH);

  // Calculate the investment
  float fTotal = wizData.m_fAmount;
  for (int i = 0; i < wizData.m_nPeriod; i++)
    fTotal += (fTotal * (wizData.m_fReturnRate / 100));

  // Display the result
  CString sResult;
  sResult.Format(“%s %s. %s’s total investment “
    “after %d years is $%.2f.”,
    wizData.m_sFName, wizData.m_sMInitial, wizData.m_sLName,
    wizData.m_nPeriod, fTotal);
  SetDlgItemText(IDC_RESULT, sResult);

  return TRUE;
}

// CInvestWizPage3 Message Handlers
void CInvestWizPage3::OnPaint() {
  CPaintDC dc(this);

  // Select the bitmap into a memory DC
  CDC dcMem;
  dcMem.CreateCompatibleDC(&dc);
  dcMem.SelectObject(&m_bitmap);

  // Copy the bitmap to the dialog DC
  dc.BitBlt(0, 0, 129, 226, &dcMem, 0, 0, SRCCOPY);
}

The most important difference to note between all the wizard page classes is the wizard buttons that each of them displays. Also, note that the third page doesn’t provide a DoDataExchange() member function because it is an output-only page and doesn’t need to store user input.

The Wizard

The majority of the work in a wizard is in creating the wizard pages. This means that an actual wizard class requires relatively little code. Listing 8.23 contains the class declaration for the CInvestmentWiz wizard class.

Listing 8.23 The CInvestmentWiz Class Declaration


class CInvestmentWiz : public CPropertySheet {
  // Private Member Data
private:
  CInvestWizPage1 m_page1;
  CInvestWizPage2 m_page2;
  CInvestWizPage3 m_page3;

  // Public Constructor(s)/Destructor
public:
                CInvestmentWiz();

  // Message Map & Runtime Support
protected:
  DECLARE_MESSAGE_MAP()
};

The CInvestmentWiz wizard class contains the individual wizard pages as member objects. This handles their initial creation, but it is still necessary to call the Construct() member function on each page to properly construct the pages. This is carried out in the constructor for CInvestmentWiz, which is shown in Listing 8.24.

Listing 8.24 The CInvestmentWiz Class Implementation


// CInvestmentWiz Message Map & Runtime Support
BEGIN_MESSAGE_MAP(CInvestmentWiz, CPropertySheet)
END_MESSAGE_MAP()

// CInvestmentWiz Public Constructor(s)/Destructor
CInvestmentWiz::CInvestmentWiz() : CPropertySheet() {
  // Create the investment wizard
  Construct(“Investment Wizard”, this);

  // Add the wizard pages
  m_page1.Construct(IDD_INVESTWIZPAGE1, 0);

  m_page2.Construct(IDD_INVESTWIZPAGE2, 0);
  m_page3.Construct(IDD_INVESTWIZPAGE3, 0);
  AddPage(&m_page1);
  AddPage(&m_page2);
  AddPage(&m_page3);

  // Set the wizard global variable for page access
  pInvestmentWiz = this;
}

The CInvestmentWiz class implementation consists solely of a message map and a constructor. The constructor constructs each wizard page and then adds them to the wizard through the AddPage() member function. It then sets the global wizard pointer variable, pInvestmentWiz. As the code demonstrates, the wizard class intelligently delegates most of its functionality to the wizard pages.

Using the Investment Wizard is very straightforward. Following is the code necessary to invoke the wizard from an application:

CInvestmentWiz wiz;
wiz.SetWizardMode();
wiz.DoModal();

Of course, the application containing this code would also need to include the wizard’s resources in its resource script.



Splitting a View

It is sometimes useful to support multiple views in order to provide an alternative means of viewing or modifying document data. A good way to manage multiple views is to use a splitter window, which divides the client area of an application into separate panes. Each pane of a splitter window contains a view. You can use the splitter window to adjust the sizes of the views. Figure 8.6 should give you an idea of how a splitter window supports multiple views.

It’s relatively easy to add a splitter window to an MFC application. The first step is to declare a member variable of type CSplitterWnd:

CSplitterWnd  m_wndSplitter;

The CSplitterWnd class is defined in the AfxCView.h header file, which should be added to the application’s StdAfx.h header file. The splitter window should be created in the OnCreateClient() member function of CMainFrame because the splitter window resides in the client area of CMainFrame. Following is the prototype for the OnCreateClient() member function:

virtual BOOL OnCreateClient(LPCREATESTRUCT lpcs,
  CCreateContext* pContext);


Figure 8.6  A splitter window used to support multiple views.

Listing 8.25 contains the source code for an OnCreateClient() member function that creates the splitter window as well as the two views residing in each splitter pane.

Listing 8.25 An OnCreateClient() Member Function that Creates a Splitter Window


BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*
  pContext) {
  // Create the splitter window and views
  m_wndSplitter.CreateStatic(this, 1, 2);
  m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CMyView),
    CSize(400, 400), pContext);
  m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CYourView),
    CSize(400, 400), pContext);
  return TRUE;
}

The CreateStatic() member function is used to create the splitter window. The second and third parameters to this function indicate the number of rows and columns of panes in the splitter window. A 1×2 splitter window consists of two panes separated by a vertical splitter bar. The two calls to CreateView() create the views associated with each pane. The left pane (0, 0) is of type CMyView, which is a hypothetical view class. The right pane (0, 1) is of type CYourView, another hypothetical view class. Both views are given a size of 400×400 upon creation, which is arbitrary.


Note:  

The CSplitterWnd class also supports a Create() member function that is used to create dynamic splitter windows. Dynamic splitter windows are limited to supporting views of the same class. The CreateStatic() member function must be used to create a static splitter window if you want to use views of differing types.


You might have reason to want to keep the user from being able to resize the panes managed by a splitter window. In this case, you don’t want the user to be able to move the divider separating the panes of the splitter window. To carry out this functionality, you have to understand that the functionality of the divider is handled automatically by the CSplitterWnd class. So, to alter this functionality, you must derive a class from CSplitterWnd and override the appropriate message handlers that control the divider. Following are the three message handlers that you must override in your derived class, which you might call CFixedSplitterWnd:

  OnLButtonDown()
  OnMouseMove()
  OnSetCursor()

The key to dodging the default functionality provided by CSplitterWnd is to not allow these message handlers to call up to the parent versions in CSplitterWnd. Rather, you should call the generic versions of these handlers in the CWnd class. Following is a suitable implementation for the OnLButtonDown() message handler, which demonstrates how this is accomplished:

void CFixedSplitterWnd::OnLButtonDown(UINT nFlags, CPoint point) {
  CWnd::OnLButtonDown(nFlags, point);
}

Similar implementations for the OnMouseMove() and OnSetCursor() message handlers will finish up the CFixedSplitterWnd class.

Summary

This chapter tackles a wide range of topics dealing with user interfaces and how to extend them. In the beginning of the chapter, you learned how to respond to user input from the keyboard and mouse. You then learned about the standard user interface features included in applications generated by AppWizard. From there, you explored menus and how to modify them programmatically. Control bars were next on the agenda; you learned how to use toolbars, status bars, and ToolTips.

In the next part of the chapter, you shifted gears a little when you were introduced to MFC’s command update mechanism. However, things quickly turned graphical again when you learned how to design and implement property sheets and wizards. In the last part of the chapter, you were introduced to splitter windows and how they allow you to present multiple views.